In this article, I will discuss an innovative method for developing a Java-based application that is compiled into a native image with Quarkus. This native image serves as the core application, which is then containerized and deployed within an Amazon Elastic Container Service (ECS) and Amazon Elastic Kubernetes Service (EKS) cluster using AWS Fargate.
Amazon ECS is a fully managed service for container orchestration, while Amazon EKS provides a fully managed Kubernetes experience. Both services leverage Fargate for serverless computing, eliminating the need to provision and manage servers, allowing you to specify and pay for resources based on your application’s needs, and enhancing security through inherent application isolation. AWS Lambda, another serverless compute service, automatically handles your code execution in response to events.
Quarkus is a Supersonic Subatomic Java framework that utilizes OpenJDK HotSpot and GraalVM, along with over fifty different libraries such as RESTEasy, Vertx, Hibernate, and Netty. In a previous installment, I illustrated how GraalVM can optimize Docker image size. GraalVM is an open-source, high-performance polyglot virtual machine from Oracle, which I leverage to compile native images in advance, thus improving startup speed and reducing memory usage and file size for Java Virtual Machine (JVM)-based applications. This ahead-of-time compilation is made possible by a framework known as Substrate.
Application Architecture
To start, take a look at the GitHub repository that contains the demo application. Our application is a straightforward REST-based Create, Read, Update, Delete (CRUD) service that manages basic user functionalities. All data is stored in an Amazon DynamoDB table. Quarkus provides an extension for Amazon DynamoDB, which is built on AWS SDK for Java V2. This extension supports both blocking and asynchronous programming models. For local development, you can use DynamoDB Local, a downloadable version that allows you to write and test applications without needing access to the live DynamoDB service. Once you’re set for production, you can make minor adjustments to your code to connect to the actual DynamoDB service.
The REST functionality is implemented in the UserResource class, which uses the JAX-RS implementation RESTEasy. This class calls the UserService, which contains the methods to access a DynamoDB table using the AWS SDK for Java. User-related data is encapsulated in a Plain Old Java Object (POJO) named User.
Building the Application
To create a Docker container image for use in the task definition of my ECS cluster, follow these three steps: build the application, create the Docker container image, and push the image to a Docker image registry.
For building the application, I used Maven with distinct profiles. The default profile generates an uber JAR—a self-contained application with all dependencies—ideal for local testing due to shorter build times compared to native-image builds. Executing the package command will also run all tests, necessitating that DynamoDB Local is running on your workstation.
$ docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -inMemory -sharedDb
$ mvn package
The second profile compiles the application into a native image using GraalVM. Here, you use the native image as the foundation for a Docker container. The Dockerfile can be found at src/main/docker/Dockerfile.native, utilizing a multi-stage build pattern.
FROM quay.io/quarkus/ubi-quarkus-native-image:20.1.0-java11 as nativebuilder
RUN mkdir -p /tmp/ssl-libs/lib
&& cp /opt/graalvm/lib/security/cacerts /tmp/ssl-libs
&& cp /opt/graalvm/lib/libsunec.so /tmp/ssl-libs/lib/
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY target/*-runner /work/application
COPY --from=nativebuilder /tmp/ssl-libs/ /work/
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0", "-Djava.library.path=/work/lib", "-Djavax.net.ssl.trustStore=/work/cacerts"]
For the second and third steps, build and push the Docker image to a registry of your choice:
$ docker build -f src/main/docker/Dockerfile.native -t <repo/image:tag>
$ docker push <repo/image:tag>
Setting Up the Infrastructure
With the application compiled to a native image and the Docker image built, it’s time to establish the essential infrastructure. This includes an Amazon Virtual Private Cloud (VPC), an Amazon ECS or EKS cluster with AWS Fargate launch type, an Amazon DynamoDB table, and an Application Load Balancer.
By codifying your infrastructure, you can treat it like code. In this scenario, we utilize the AWS Cloud Development Kit (AWS CDK), an open-source framework to model and provision your cloud application resources using familiar programming languages. The CDK application code is available in the demo repository under eks_cdk/lib/ecs_cdk-stack.ts. To set up the infrastructure in the AWS region us-east-1, execute:
$ npm install -g aws-cdk // Install the CDK
$ cd ecs_cdk
$ npm install // Retrieves dependencies for the CDK stack
$ npm run build // Compiles the TypeScript files to JavaScript
$ cdk deploy // Deploys the CloudFormation stack
The resulting AWS CloudFormation stack output includes the DNS record for the load balancer. The core of our infrastructure is the Amazon ECS or EKS cluster with the AWS Fargate launch type.
For more detailed instructions, check out this blog post that further elaborates on the topic. Additionally, for authoritative insights, visit this resource to deepen your understanding. If you’re looking for excellent career opportunities, explore this position that could be ideal for you.
Leave a Reply